agora inbox for [email protected]
help / color / mirror / Atom feedPLpgSQL Stat Problem
1739+ messages / 3 participants
[nested] [flat]
* PLpgSQL Stat Problem
@ 1999-04-29 18:27 Andy Lewis <[email protected]>
1999-04-29 22:39 ` Re: [HACKERS] PLpgSQL Stat Problem Tom Lane <[email protected]>
0 siblings, 1 reply; 1739+ messages in thread
From: Andy Lewis @ 1999-04-29 18:27 UTC (permalink / raw)
To: pgsql-hackers
I posted the following message in [GENERAL] and still haven't been able to
resolve the problem.
The file permissions are set to postgres and postgres is authorized to
access /usr/local/pgsql/lib.
What I am trying to do is insert a row into a DB but, I want to be able to
check each row for a specified column prior to insert to see if there is a
duplicate column.
If there is I want to be able to abort or simply skip that insert. If
there are any easier ways to do this please let me know.
I'm running Postgres 6.4.2 on Linux.
Any ideas would be greatly appreciated.
Please email any resonses to the above email address as I don't belong to
[HACKERS].
Thanks in advance.
Andy
> I got the following error after trying to copy the example at:
> http://www.postgresql.org/mhonarc/pgsql-sql/1999-04/msg00076.html
>
> ----------------------------------------------------------
> test=> select a(pin,first_name) from ibs_subscriber ;
> ERROR: stat failed on file ${exec_prefix}/lib/plpgsql.so
> ----------------------------------------------------------
>
> The file plpgsql.so does exist in /usr/local/pgsql/lib.
> Any ideas?
^ permalink raw reply [nested|flat] 1739+ messages in thread
* Re: [HACKERS] PLpgSQL Stat Problem
1999-04-29 18:27 PLpgSQL Stat Problem Andy Lewis <[email protected]>
@ 1999-04-29 22:39 ` Tom Lane <[email protected]>
1999-04-29 23:23 ` Re: [HACKERS] PLpgSQL Stat Problem Andy Lewis <[email protected]>
0 siblings, 1 reply; 1739+ messages in thread
From: Tom Lane @ 1999-04-29 22:39 UTC (permalink / raw)
To: Andy Lewis <[email protected]>; +Cc: pgsql-hackers
Andy Lewis <[email protected]> writes:
>> I got the following error after trying to copy the example at:
>> http://www.postgresql.org/mhonarc/pgsql-sql/1999-04/msg00076.html
>>
>> test=> select a(pin,first_name) from ibs_subscriber ;
>> ERROR: stat failed on file ${exec_prefix}/lib/plpgsql.so
Looks like the system is trying to use the literal filename
"${exec_prefix}/lib/plpgsql.so", which of course is not right.
I'm guessing that when you copied the CREATE FUNCTION out of the
example, you just cut and pasted without doing the shell variable
expansion that was supposed to happen to replace ${exec_prefix}
with /usr/local/pgsql ...
regards, tom lane
^ permalink raw reply [nested|flat] 1739+ messages in thread
* Re: [HACKERS] PLpgSQL Stat Problem
1999-04-29 18:27 PLpgSQL Stat Problem Andy Lewis <[email protected]>
1999-04-29 22:39 ` Re: [HACKERS] PLpgSQL Stat Problem Tom Lane <[email protected]>
@ 1999-04-29 23:23 ` Andy Lewis <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Andy Lewis @ 1999-04-29 23:23 UTC (permalink / raw)
To: Tom Lane <[email protected]>; +Cc: pgsql-hackers
Solved! Thanks.
I know this is dumb but, I ran mklang.sql on a test DB and in that file it
references: ${exec_prefix}
Without thinking to remove the DB and start from scratch.
Anyway thanks to all.
Andy
On Thu, 29 Apr 1999, Tom Lane wrote:
> Andy Lewis <[email protected]> writes:
> >> I got the following error after trying to copy the example at:
> >> http://www.postgresql.org/mhonarc/pgsql-sql/1999-04/msg00076.html
> >>
> >> test=> select a(pin,first_name) from ibs_subscriber ;
> >> ERROR: stat failed on file ${exec_prefix}/lib/plpgsql.so
>
> Looks like the system is trying to use the literal filename
> "${exec_prefix}/lib/plpgsql.so", which of course is not right.
>
> I'm guessing that when you copied the CREATE FUNCTION out of the
> example, you just cut and pasted without doing the shell variable
> expansion that was supposed to happen to replace ${exec_prefix}
> with /usr/local/pgsql ...
>
> regards, tom lane
>
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1739+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1739+ messages in thread
end of thread, other threads:[~2026-04-27 14:23 UTC | newest]
Thread overview: 1739+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
1999-04-29 18:27 PLpgSQL Stat Problem Andy Lewis <[email protected]>
1999-04-29 22:39 ` Tom Lane <[email protected]>
1999-04-29 23:23 ` Andy Lewis <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 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